/*
 * Copyright (c) 2018 Hitevision
 */

package tw.com.hitevision.whiteboard.android.state;


import android.content.Context;
import android.graphics.PointF;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;

import androidx.annotation.ColorInt;
import tw.com.hitevision.whiteboard.android.Whiteboard;
import tw.com.hitevision.whiteboard.android.render.BaseItem;
import tw.com.hitevision.whiteboard.android.render.Word;

/**
 * This class is used to record the movement user took, which means what have been DONE
 */
public class Action {
    private static final String TAG = Action.class.getSimpleName();

    private Type type;
    private PointF pinnedPoint;
    private long[] insertedIds;
    private int[] itemIndexes;
    private PointF positionDifference;
    private float[] scaleRateArrayX;
    private float[] scaleRateArrayY;
    private float rotateAngle;
    private List<BaseItem> stateItems;
    @ColorInt
    private int[] colorBuffer;

    private List<BaseItem> redoItems;
    private int[] redoIndexes;

    private List<CanvasAction> canvasUndoActions;
    private List<CanvasAction> canvasRedoActions;

    private Action(Type type,
                   long[] insertedIds,
                   int[] itemIndexes,
                   PointF pinnedP,
                   float[] rateX,
                   float[] rateY,
                   PointF posDiff,
                   float rotate,
                   List<BaseItem> items,
                   int[] color) {

        this.type = type;
        this.insertedIds = insertedIds;
        this.itemIndexes = itemIndexes;
        this.pinnedPoint = pinnedP;
        this.scaleRateArrayX = rateX;
        this.scaleRateArrayY = rateY;
        this.positionDifference = posDiff;
        this.rotateAngle = rotate;
        this.stateItems = items;
        this.colorBuffer = color;
    }

    public Type getType() {
        return type;
    }

    public enum Type {
        MOVE, ROTATE, SCALE, DELETE, INSERT, ERASE, ROSCALE, TEXT, TEXT_REMOVE,
        PUSH_END, COLOR, STICKY_NOTE, TEXT_SCALE, TRIMMING
    }

    public static Action createScaleAction(long[] id, PointF pinnedPoint, float rateX, float rateY) {
        PointF p = new PointF(pinnedPoint.x, pinnedPoint.y);
        return new Action(Type.SCALE, id, null, p, new float[] {rateX}, new float[] {rateY}, null, 0, null, null);
    }

    public static Action createRotateAction(long[] id, PointF pinnedPoint, float angle) {
        PointF p = new PointF(pinnedPoint.x, pinnedPoint.y);
        return new Action(Type.ROTATE, id, null, p, null, null, null, angle, null, null);
    }

    public static Action createMoveAction(long[] id, float diffX, float diffY) {
        return new Action(Type.MOVE, id, null, null, null, null, new PointF(diffX, diffY), 0, null, null);
    }

    public static Action createDeleteAction(int[] id, List<BaseItem> lines) {
        List<BaseItem> items = new ArrayList<>(lines);
        return new Action(Type.DELETE, null, id, null, null, null, null, 0, items, null);
    }

    public static Action createInsertAction(long... ids) {
        return new Action(Type.INSERT, ids, null, null, null, null, null, 0, null, null);
    }

    public static Action createEraserAction(int[] deletedList, int[] sizeList, List<BaseItem> items) {
        List<BaseItem> itemList = new ArrayList<>(items);
        return new Action(Type.ERASE, null, deletedList, null, null, null, null, 0, itemList, sizeList);
    }

    public static Action createRoscaleAction(long[] id, PointF moveDiff, float degree, float scaleRateX, float scaleRateY, PointF pinnedPoint) {
        PointF m = new PointF(moveDiff.x, moveDiff.y);
        PointF p = new PointF(pinnedPoint.x, pinnedPoint.y);
        return new Action(Type.ROSCALE, id, null, p, new float[] {scaleRateX}, new float[] {scaleRateY}, m, degree, null, null);
    }

    public static Action createUndoTextAction(long id) {
        return new Action(Type.TEXT, new long[] {id}, null, null, null, null, null, 0, null, null);
    }

    public static Action createDeleteTextAction(int id, BaseItem item) {
        List<BaseItem> items = new ArrayList<>();
        items.add(item);
        return new Action(Type.TEXT_REMOVE, null, new int[] {id}, null, null, null, null, 0, items, null);
    }

    public static Action createPushEndAction(int[] list) {
        return new Action(Type.PUSH_END, null, list, null, null, null, null, 0, null, null);
    }

    public static Action createColorChangeAction(int[] id, @ColorInt int[] colorBuffer) {
        return new Action(Type.COLOR, null, id, null, null, null, null, 0, null, colorBuffer);
    }

    public static Action createStickyNoteAction(long[] id, List<BaseItem> lineState, int[] indexes) {
        List<BaseItem> items = new ArrayList<>(lineState);
        return new Action(Type.STICKY_NOTE, id, indexes, null, null, null, null, 0, items, null);
    }

    public static Action createTrimmingAction(long[] id, List<BaseItem> lineState, int[] indexes) {
        List<BaseItem> items = new ArrayList<>(lineState);
        return new Action(Type.TRIMMING, id, indexes, null, null, null, null, 0, items, null);
    }

    public static Action createTextScaleAction(long id) {
        return new Action(Type.TEXT_SCALE, new long[] {id}, null, null, null, null, null, 0, null, null);
    }

    private BaseItem findItem(List<BaseItem> items, long id) {
        for (BaseItem item : items) {
            if (item.getId() == id) {
                return item;
            }
        }
        return null;
    }

    private void applyCanvasTransform(List<CanvasAction> canvasActions, BaseItem item, Transform transform) {
        if (canvasActions != null) {
            for (CanvasAction canvasAction : canvasActions) {
                transform.setTranslate(canvasAction.scrollX, canvasAction.scrollY);
                transform.setScalePoint(canvasAction.zoomPointF.x, canvasAction.zoomPointF.y);
                transform.setScale(canvasAction.zoomScale, canvasAction.zoomScale);
                transform.apply(item);
                transform.clear();
            }
        }
    }

    public void undo(List<BaseItem> items, Context context, Whiteboard.Input input) {
        Transform transform = new Transform(context);
        switch (type) {
            case MOVE: {
                transform.moveBy(-positionDifference.x, -positionDifference.y);
                for (long id : insertedIds) {
                    final BaseItem item = findItem(items, id);
                    if (item != null) {
                        transform.apply(item);
                    }
                }
                break;
            }
            case SCALE: {
                transform.setScale(1 / scaleRateArrayX[0], 1 / scaleRateArrayY[0]);
                transform.setScalePoint(pinnedPoint.x, pinnedPoint.y);
                for (long id : insertedIds) {
                    final BaseItem item = findItem(items, id);
                    if (item != null) {
//                        if (item instanceof Word) {
//                            ((Word) item).loadWordState();
//                        } else {
//                            transform.apply(item);
//                        }
                        transform.apply(item);
                    }
                }
                break;
            }
            case INSERT: {
                redoItems = new ArrayList<>();
                redoIndexes = new int[insertedIds.length];

                for (int i = 0; i < insertedIds.length; i++) {
                    final long id = insertedIds[i];

                    for (int index = 0, size = items.size(); index < size; index++) {
                        final BaseItem item = items.get(index);

                        if (item.getId() == id) {
                            items.remove(index);
                            redoItems.add(item);
                            redoIndexes[i] = index;
                            break;
                        }
                    }
                }
                break;
            }
            case DELETE: {
                for (int i = 0; i < itemIndexes.length; i++) {
                    int itemIndex = itemIndexes[i];
                    BaseItem item = stateItems.get(i);
                    item.setSelected(false);
                    if (item instanceof Word) {
                        ((Word) item).setSelectionAreaVisible(input == Whiteboard.Input.TEXT);
                    }
                    items.add(itemIndex, item);
                }
                break;
            }
            case ROSCALE: {
                transform.setRotatePoint(pinnedPoint.x, pinnedPoint.y);
                transform.setRotate(-rotateAngle);
                transform.setScalePoint(pinnedPoint.x, pinnedPoint.y);
                transform.setScale(1 / scaleRateArrayX[0], 1 / scaleRateArrayY[0]);
//                transform.moveBy(-this.positionDifference.x, -this.positionDifference.y);
                for (long id : insertedIds) {
                    final BaseItem item = findItem(items, id);
                    if (item != null) {
//                        if (item instanceof Word) {
//                            Word w = (Word) item;
//                            w.rotateFromPoint(-rotateAngle, pinnedPoint);
//                            w.loadWordState();
//                        } else {
//                            transform.apply(item);
//                        }
                        transform.apply(item);
                    }
                }
                break;
            }
            case ERASE:{
                int[] deletedArray = itemIndexes;
                int[] sizeArray = colorBuffer;
                List<BaseItem> deletedItems = stateItems;

                if (deletedArray.length != deletedItems.size()) {
                    return;
                }

                redoItems = new ArrayList<>();

                for (int ii = deletedItems.size() - 1; ii >= 0; ii--) {
                    BaseItem deletedItem = deletedItems.get(ii);
                    int deleted = deletedArray[ii];
                    int size = sizeArray[ii];

                    for (int i = 0; i < size; i++) {
                        BaseItem item = items.get(deleted);
                        items.remove(item);
                        redoItems.add(0, item);
                    }

                    items.add(deleted, deletedItem);
                }

                break;
            }
            case ROTATE: {
                transform.setRotatePoint(pinnedPoint.x, pinnedPoint.y);
                transform.setRotate(-rotateAngle);
                for (long id : insertedIds) {
                    final BaseItem item = findItem(items, id);
                    if (item != null) {
                        transform.apply(item);
                    }
                }
                break;
            }
            case TEXT: {
                if (insertedIds.length != 1) {
                    log("Case TEXT, the size of inserted items is not equal to 1:" + insertedIds.length);
                    break;
                }
                for (int i = items.size() - 1; i >= 0; i--) {
                    if (items.get(i).getId() == insertedIds[0] && items.get(i) instanceof Word) {
                        Word w = (Word) items.get(i);
                        w.loadWordState();
                        applyCanvasTransform(canvasUndoActions, w, transform);
                        w.setTextStatus(Word.TextStatus.SELECTED);
                        break;
                    }
                }
                if (canvasRedoActions != null) {
                    canvasRedoActions.clear();
                }
                break;
            }
            case TEXT_REMOVE: {
                /* Use TEXT_REMOVE to distinguish from the normal delete case. */
                if (itemIndexes.length != 1) {
                    log("Case TEXT_REMOVE, the size of deleted Items is not equal to 1:" + itemIndexes.length);
                    break;
                }
                int itemIndex = itemIndexes[0];
                BaseItem item = stateItems.get(0);
                if (item instanceof Word) {
                    ((Word) item).loadWordState();
                    applyCanvasTransform(canvasUndoActions, item, transform);
                    ((Word) item).setTextStatus(Word.TextStatus.UNFOCUSED);
                    items.add(itemIndex, item);
                }
                if (canvasRedoActions != null) {
                    canvasRedoActions.clear();
                }
                break;
            }
            case PUSH_END: {
                int size = itemIndexes.length;

                List<BaseItem> buffer = new ArrayList<>();
                for (int i = 0; i < size; i++) {
                    buffer.add(items.get(i));
                }
                items.removeAll(buffer);
                for (int i = 0; i < size; i++) {
                    if (itemIndexes[i] < items.size()) {
                        items.add(itemIndexes[i], buffer.get(i));
                    } else if (itemIndexes[i] == items.size()) {
                        items.add(buffer.get(i));
                    } else {
                        log("Error Case PUSH_END, wrong inserted id");
                        break;
                    }
                }
                break;
            }
            case COLOR: {
                // color change use sequence id to record, so use delete Id
                for (int i = 0; i < itemIndexes.length; i++) {
                    int index = itemIndexes[i];
                    int tempColor = items.get(index).getColor();
                    items.get(index).setColor(colorBuffer[i]);
                    colorBuffer[i] = tempColor;
                }
                break;
            }
            case STICKY_NOTE: {
                redoItems = new ArrayList<>();

                for (int i = 0; i < itemIndexes.length; i++) {
                    int index = itemIndexes[i];
                    BaseItem item = stateItems.get(i);
                    item.setSelected(false);
                    if (item instanceof Word) {
                        ((Word) item).setSelectionAreaVisible(input == Whiteboard.Input.TEXT);
                    }
                    items.add(index, item);
                }

                for (long id : insertedIds) {
                    for (int i = 0, size = items.size(); i < size; i++) {
                        BaseItem item = items.get(i);
                        if (item.getId() == id) {
                            redoItems.add(items.remove(i));
                            break;
                        }
                    }
                }
                break;
            }
            case TRIMMING: {
                redoItems = new ArrayList<>();
                redoIndexes = new int[insertedIds.length];

                for (int i = 0; i < itemIndexes.length; i++) {
                    int itemIndex = itemIndexes[i];
                    BaseItem item = stateItems.get(i);
                    item.setSelected(false);
                    items.add(itemIndex, item);
                }

                for (int j = 0, length = insertedIds.length; j < length; j++) {
                    final long id = insertedIds[j];
                    for (int i = 0, size = items.size(); i < size; i++) {
                        final BaseItem item = items.get(i);
                        if (item.getId() == id) {
                            redoItems.add(items.remove(i));
                            redoIndexes[j] = i;
                            break;
                        }
                    }
                }
                break;
            }
            case TEXT_SCALE: {
                if (insertedIds.length != 1) {
                    log("Case TEXT_SCALE, the size of action Items is not equal to 1:" + insertedIds.length);
                }
                final BaseItem item = findItem(items, insertedIds[0]);
                if (item != null && item instanceof Word) {
                    ((Word) item).loadWordState();
                    applyCanvasTransform(canvasUndoActions, item, transform);
                }
                if (canvasRedoActions != null) {
                    canvasRedoActions.clear();
                }
                break;
            }
            default: {
                // do nothing
                break;
            }
        }
    }

    public void redo(List<BaseItem> items, Context context, Whiteboard.Input input) {
        log("redo: type=" + type);
        Transform transform = new Transform(context);
        switch (type) {
            case MOVE: {
                transform.moveBy(positionDifference.x, positionDifference.y);
                for (long id : insertedIds) {
                    final BaseItem item = findItem(items, id);
                    if (item != null) {
                        transform.apply(item);
                    }
                }
                break;
            }
            case SCALE: {
                transform.setScale(scaleRateArrayX[0], scaleRateArrayY[0]);
                transform.setScalePoint(pinnedPoint.x, pinnedPoint.y);
                for (long id : insertedIds) {
                    final BaseItem item = findItem(items, id);
                    if (item != null) {
//                        if (item instanceof Word) {
//                            ((Word) item).loadWordState();
//                        } else {
//                            transform.apply(item);
//                        }
                        transform.apply(item);
                    }
                }
                break;
            }
            case INSERT: {
                for (int i = 0, size = redoItems.size(); i < size; i++) {
                    final int index = redoIndexes[i];
                    final BaseItem item = redoItems.get(i);
                    item.setSelected(false);
                    if (item instanceof Word) {
                        ((Word) item).setSelectionAreaVisible(input == Whiteboard.Input.TEXT);
                    }
                    items.add(item);
//                    items.add(index, item);
                }
                break;
            }
            case DELETE: {
                for (int i = itemIndexes.length - 1; i >= 0; i--) {
                    final int index = itemIndexes[i];
                    if (index < items.size()) {
                        items.remove(index);
                    }
                }
                break;
            }
            case ERASE:{
                int[] deletedArray = itemIndexes;
                int[] sizeArray = colorBuffer;
                List<BaseItem> deletedItems = stateItems;

                for (int ii = 0; ii < deletedItems.size(); ii++) {
                    int deleted = deletedArray[ii];
                    int size = sizeArray[ii];

                    items.remove(deleted);
                    for (int i = 0; i < size; i++) {
                        items.add(deleted,  redoItems.remove(0));
                    }
                }

                break;
            }
            case ROSCALE: {
                transform.setRotatePoint(pinnedPoint.x, pinnedPoint.y);
                transform.setRotate(rotateAngle);
                transform.setScalePoint(pinnedPoint.x, pinnedPoint.y);
                transform.setScale(scaleRateArrayX[0], scaleRateArrayY[0]);
//                transform.moveBy(this.positionDifference.x, this.positionDifference.y);
                for (long id : insertedIds) {
                    final BaseItem item = findItem(items, id);
                    if (item != null) {
//                        if (item instanceof Word) {
//                            Word w = (Word) item;
//                            w.rotateFromPoint(rotateAngle, pinnedPoint);
//                            w.loadWordState();
//                        } else {
//                            transform.apply(item);
//                        }
                        transform.apply(item);
                    }
                }
                break;
            }
            case ROTATE: {
                transform.setRotatePoint(pinnedPoint.x, pinnedPoint.y);
                transform.setRotate(rotateAngle);
                for (long id : insertedIds) {
                    final BaseItem item = findItem(items, id);
                    if (item != null) {
                        transform.apply(item);
                    }
                }
                break;
            }
            case TEXT: {
                if (insertedIds.length != 1) {
                    log("Case TEXT, the size of inserted items is not equal to 1:" + insertedIds.length);
                    break;
                }

                for (int i = items.size() - 1; i >= 0; i--) {
                    if (items.get(i).getId() == insertedIds[0] && items.get(i) instanceof Word) {
                        Word w = (Word) items.get(i);
                        w.redoWordState();
                        applyCanvasTransform(canvasRedoActions, w, transform);
                        w.setTextStatus(Word.TextStatus.SELECTED);
                        break;
                    }
                }
                if (canvasUndoActions != null) {
                    canvasUndoActions.clear();
                }
                break;
            }
            case TEXT_REMOVE: {
                /* Use TEXT_REMOVE to distinguish from the normal delete case. */
                if (itemIndexes.length != 1) {
                    log("Case TEXT_REMOVE, the size of deleted Items is not equal to 1:" + itemIndexes.length);
                    break;
                }
                int itemIndex = itemIndexes[0];
                BaseItem item = stateItems.get(0);
                if (item instanceof Word) {
                    ((Word) item).redoWordState();
                    ((Word) item).setTextStatus(Word.TextStatus.UNFOCUSED);
                    items.add(itemIndex, item);
                    applyCanvasTransform(canvasRedoActions, item, transform);
                }
                if (canvasUndoActions != null) {
                    canvasUndoActions.clear();
                }
                break;
            }
            case PUSH_END: {
                List<BaseItem> buffer = new ArrayList<>();
                for (int i = itemIndexes.length - 1; i >= 0; i--) {
                    int index = itemIndexes[i];
                    buffer.add(items.remove(index));
                }
                buffer.addAll(items);
                items.clear();
                items.addAll(buffer);
                break;

            }
            case COLOR: {
                for (int i = 0; i < itemIndexes.length; i++) {
                    int index = itemIndexes[i];
                    int tempColor = items.get(index).getColor();
                    items.get(index).setColor(colorBuffer[i]);
                    colorBuffer[i] = tempColor;
                }
                break;
            }
            case STICKY_NOTE: {
                items.addAll(redoItems);
                items.removeAll(stateItems);
                break;
            }
            case TRIMMING: {
                items.removeAll(stateItems);

                for (int i = 0, length = redoIndexes.length; i < length; i++) {
                    int id = redoIndexes[i];
                    BaseItem item = redoItems.get(i);
                    if(id<=items.size())
                        items.add(id,item);
                    else
                        items.add(item);
                }
                break;
            }
            case TEXT_SCALE: {
                if (insertedIds.length != 1) {
                    log("Case TEXT_SCALE, the size of action Items is not equal to 1:" + insertedIds.length);
                }
                final BaseItem item = findItem(items, insertedIds[0]);
                if (item instanceof Word) {
                    ((Word) item).redoWordState();
                    applyCanvasTransform(canvasRedoActions, item, transform);
                }
                if (canvasUndoActions != null) {
                    canvasUndoActions.clear();
                }
                break;
            }
            default: {
                // do nothing
                break;
            }
        }
    }

    void updateUndoActionTransform(float scrollX, float scrollY, float zoomScale, PointF zoomPointF, List<BaseItem> items) {
        switch (type) {
            case MOVE: {
                positionDifference.x *= zoomScale;
                positionDifference.y *= zoomScale;
                break;
            }
            case SCALE:
            case ROTATE:
            case ROSCALE: {
                pinnedPoint.x = zoomPointF.x + (pinnedPoint.x - zoomPointF.x) * zoomScale;
                pinnedPoint.y = zoomPointF.y + (pinnedPoint.y - zoomPointF.y) * zoomScale;
                pinnedPoint.x += scrollX;
                pinnedPoint.y += scrollY;
                break;
            }
            case DELETE:
            case TRIMMING:
            case STICKY_NOTE: {
                for (int i = 0; i < itemIndexes.length; i++) {
                    BaseItem item = stateItems.get(i);
                    item.scaleFromPoint(zoomScale, zoomScale, zoomPointF);
                    item.moveBy(scrollX, scrollY);
                }
                break;
            }
            case TEXT_SCALE:
            case TEXT_REMOVE:
            case TEXT: {
                if (canvasUndoActions == null) {
                    canvasUndoActions = new ArrayList<>();
                }

                CanvasAction canvasAction = new CanvasAction(scrollX, scrollY, zoomScale, zoomPointF);
                canvasUndoActions.add(canvasAction);

                break;
            }
            case ERASE: {
                for (BaseItem item : stateItems) {
                    item.scaleFromPoint(zoomScale, zoomScale, zoomPointF);
                    item.moveBy(scrollX, scrollY);
                }
                break;
            }
//            case INSERT:
//            case FORWARD:
//            case BACKWARD:
//            case COLOR:
            default: {
                break;
            }
        }
    }

    void updateRedoActionTransform(float scrollX, float scrollY, float zoomScale, PointF zoomPointF, List<BaseItem> items) {
        switch (type) {
            case MOVE: {
                positionDifference.x *= zoomScale;
                positionDifference.y *= zoomScale;
                break;
            }
            case SCALE:
            case ROTATE:
            case ROSCALE: {
                pinnedPoint.x = zoomPointF.x + (pinnedPoint.x - zoomPointF.x) * zoomScale;
                pinnedPoint.y = zoomPointF.y + (pinnedPoint.y - zoomPointF.y) * zoomScale;
                pinnedPoint.x += scrollX;
                pinnedPoint.y += scrollY;
                break;
            }
            case STICKY_NOTE:
            case TRIMMING:
            case INSERT: {
                for (int i = 0, size = redoItems.size(); i < size; i++) {
                    final BaseItem item = redoItems.get(i);
                    item.scaleFromPoint(zoomScale, zoomScale, zoomPointF);
                    item.moveBy(scrollX, scrollY);
                }
                break;
            }
            case TEXT_SCALE:
            case TEXT_REMOVE:
            case TEXT: {
                if (canvasRedoActions == null) {
                    canvasRedoActions = new ArrayList<>();
                }

                CanvasAction canvasAction = new CanvasAction(scrollX, scrollY, zoomScale, zoomPointF);
                canvasRedoActions.add(canvasAction);

                break;
            }
            case ERASE: {
                for (BaseItem item : redoItems) {
                    item.scaleFromPoint(zoomScale, zoomScale, zoomPointF);
                    item.moveBy(scrollX, scrollY);
                }
                break;
            }
//            case DELETE:
//            case FORWARD:
//            case BACKWARD:
//            case COLOR:
            default: {
                // do nothing
                break;
            }
        }
    }

    private static void log(String msg) {
        Log.d(TAG, msg);
    }

}
